package com.vecspace.web.servlet;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import com.vecspace.cache.LocalCache;
import com.vecspace.constant.AppConst;
import com.vecspace.utils.log.HLogger;
import com.vecspace.web.HttpContentType;
import com.vecspace.web.servlet.model.HttpRequestUtil;

/**
 * 静态资源请求的统一入口（js，css，图片等）
 * @author: wanghua
 */
public class ResourceServlet extends HttpServlet {

	private static String cacheType = "resfile";

	private static final long serialVersionUID = 28595536384779654L;

	private static String[] protectedFiles = new String[] {".java",".class",".exe",".jar",".sql",".dll",".vbs",".sh",".bat",".so",".properties",".yml"}; 
	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		this.doServlet(request, response);
	}

	@Override
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		this.doServlet(request, response);
	}

	private void doServlet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		// String ctxPath = this.getServletContext().getContextPath();
		boolean useCache = true;//默认从缓存读取数据
		if(AppConst.isDebug()) {
			useCache = false;
		}
		if("false".equals(request.getParameter("cache"))) {
			//xxx.js?cache=false明确不使用缓存
			useCache = false;
		}
		String urlPath = HttpRequestUtil.getFullPath(request);
		if(urlPath == null || urlPath.length() == 0 || urlPath.equals("/")) {
			if(welcomeFile == null || welcomeFile.length() == 0) {
				welcomeFile = "irest/view/main/index";
			}
			response.sendRedirect(welcomeFile);		
			return;
		}
		byte[] data;
		if("/clearCache".equals(urlPath) || "clearCache".equals(urlPath)) {
			LocalCache.DEFAULT().clearCache(cacheType);
			String msg = "已经清空缓存的资源文件（注意：只清除当前服务器内存的缓存，对集群内的其他服务器无效）";
			data = msg.getBytes();
			String contentType = getContentType(".html");
			response.setContentType(contentType + ";charset=" + AppConst.getCharset());
		}else {
			String queryFiles = "";
			String resourcePath = urlPath;// url-pattern必须定义为 "/"
			if("/js/files.js".equals(urlPath) || "/css/files.css".equals(urlPath)) {
				String querysttring = request.getQueryString();
				if(querysttring == null) {
					querysttring = "";
				}
				resourcePath = resourcePath + "?" + querysttring;
				queryFiles = request.getParameter("files");
				queryFiles = queryFiles.replace("..", "");//不允许使用..,防止访问到其他目录下的文件。
			}
			data = readFileBytes(resourcePath, queryFiles, useCache);
			String contentType = getContentType(resourcePath);
			response.setContentType(contentType + ";charset=" + AppConst.getCharset());
			

			SimpleDateFormat sdf = new SimpleDateFormat("EEE d MMM yyyy HH:mm:ss 'GMT'", Locale.CHINA);  
			sdf.setTimeZone(TimeZone.getTimeZone("GMT")); // 设置时区为GMT
			response.setHeader("Last-Modified", sdf.format(new Date(System.currentTimeMillis() - 1000*60*60*12)));			
			response.setHeader("Expires", sdf.format(new Date(System.currentTimeMillis() + 1000*60*60*12)));
			response.setHeader("Cache-Control", "max-age=" + 60*60);//设置资源在本地缓存时间的一个值，单位为：秒(s)
		}
		OutputStream out = response.getOutputStream();
		try {
			out.write(data, 0, data.length);
		} finally {
			out.close();
		}
	}

	private String getContentType(String filePath) {
		if(filePath == null)return HttpContentType.DefaultContentType;
		String fileExt = "";
		int dotIdx = filePath.lastIndexOf('.');
		if(dotIdx > 0) {
			fileExt = filePath.substring(dotIdx);
		}
		String contentType = HttpContentType.getContentType(fileExt);
		return contentType;
//		String contentType = "application/octet-stream";
//		String filePathLowerCase = filePath.toLowerCase();
//		if (filePathLowerCase.endsWith(".js")) {
//			contentType = "application/javascript";
//		} else if (filePathLowerCase.endsWith(".css")) {
//			contentType = "text/css";
//		} else if (filePathLowerCase.endsWith(".json")) {
//			contentType = "application/json";
//		} else if (filePathLowerCase.endsWith(".png")) {
//			contentType = "image/png";
//		} else if (filePathLowerCase.endsWith(".jpg")) {
//			contentType = "image/jpeg";
//		} else if (filePathLowerCase.endsWith(".gif")) {
//			contentType = "image/gif";
//		} else if (filePathLowerCase.endsWith(".ico")) {
//			contentType = "image/x-icon";
//		} else if (filePathLowerCase.endsWith(".html")) {
//			contentType = "text/html";
//		} else if (filePathLowerCase.endsWith(".jsp")) {
//			contentType = "text/html";
//		} else if (filePathLowerCase.endsWith(".xml")) {
//			contentType = "text/xml";
//		}else if (filePathLowerCase.endsWith(".svg")) {
//			contentType = "image/svg+xml";
//		}		
//		
//		return contentType;
	}
	
	/**根据请求的资源路径获取资源内容。如果资源不存在，则抛出异常
	 * 1、首先从本地缓存中查找资源，如果存在，则直接返回。否则读取资源，并放入缓存，然后返回资源内容
	 * 2、在请求URL后加cache=false参数，明确不从cache中读取资源。 xxx.js?cache=false
	 * 3、application.properties中定义app.debug=true时，不从缓存中读取数据
	 * @param resourcePath 资源的URL请求路径
	 * @param queryFiles 要读取的实际文件，多个文件用,分割
	 * @param useCache 是否从缓存中读取资源内容
	 * @return
	 * @throws ServletException
	 * @throws IOException 
	 * @author: wanghua
	 */
	private byte[] readFileBytes(String resourcePath, String queryFiles, boolean useCache) throws ServletException, IOException {
		if(resourcePath == null) {
			return new byte[0];
		}
		
		byte[] bytes = null;
		if(useCache){
			//从缓存读取。
			bytes = LocalCache.DEFAULT().get(cacheType, resourcePath);
			if (bytes != null) {
				return bytes;
			}
		}
		
		String resourcePathLower = resourcePath.toLowerCase();
		if(queryFiles != null && queryFiles.trim().length() > 0) {
			String[] queryFilesArr = queryFiles.split(",");
			bytes = readFiles(queryFilesArr);
		}else {
			//判断此文件是否合并多个文件（文件中的每一行代表一个文件路径）
			boolean isCombineFile = resourcePathLower.endsWith(".combine.js") 
					|| resourcePathLower.endsWith(".combine.css") 
					|| resourcePathLower.endsWith(".combine.json") 
					|| resourcePathLower.endsWith(".combine.config");
			if(isCombineFile) {
				bytes = readCombineFile(resourcePath);
			}else {
				bytes = readSingleFileBytes(resourcePath);
			}
		}
		if(bytes == null) {
			if(resourcePath.endsWith(".css.map")||resourcePath.endsWith(".js.map")) {
				bytes=new byte[0];
			}else {
				LocalCache.DEFAULT().put(cacheType, resourcePath, new byte[0]); //没有找到时也要加入缓存。避免每次访问时都查找资源。
				String errmsg = "资源不存在:" + resourcePath;
				HLogger.DEFAULT().error(errmsg);
				throw new ServletException(errmsg);
			}
		}
		LocalCache.DEFAULT().put(cacheType, resourcePath, bytes);
		return bytes;
	}
	/**资源是一个组合文件，即当前资源本身内容是多个资源的路径。读取到资源内容后，每一行解析为一个资源URL，并获取其内容。
	 * 如果combineResourcePath不存在，则返回null，资源内容为空，返回 0长度的byte数组
	 * @param combineFilePath
	 * @return 
	 * @throws IOException 
	 * @author: wanghua
	 */
	private byte[] readCombineFile(String combineResourcePath) throws IOException{
		InputStream stream = getResourceInputStream(combineResourcePath);
		if(stream != null) {
			String resourcePathText = IOUtils.toString(stream, Charset.forName("UTF-8"));
			stream.close();
			if(resourcePathText == null || resourcePathText.length() == 0) {
				return new byte[0];
			}
			String[] resourcePaths = resourcePathText.split("\n");
			return readFiles(resourcePaths);
		}else {
			return null;
		}
	}

	private byte[] readFiles(String[] resourcePaths) throws IOException {
		if(resourcePaths == null)return new byte[0];
		StringBuilder sb = new StringBuilder();
		for(String resourcePath : resourcePaths) {
			if(resourcePath == null || resourcePath.trim().length() == 0) {
				continue;
			}
			resourcePath = resourcePath.trim();
			InputStream stream2 = getResourceInputStream(resourcePath);
			String fileText = null;
			if(stream2 != null) {
				fileText = IOUtils.toString(stream2, Charset.forName("UTF-8"));
				stream2.close();
			}else {
				fileText = "/**    资源不存在：" + resourcePath + "    **/\r\n";
			}
			sb.append("/**   " + resourcePath + "    **/\r\n");
			sb.append(fileText).append("\r\n\r\n");
		}
		return sb.toString().getBytes(Charset.forName("UTF-8"));
	}

	/**根据资源访问Url读取资源内容（读取文件本身的内容）。<br>
	 * 如果资源不存在，则返回null，资源内容为空，返回 0长度的byte数组
	 * @param resourcePath
	 * @return
	 * @throws ServletException
	 * @throws IOException 
	 * @author: wanghua
	 */
	private byte[] readSingleFileBytes(String resourcePath) throws ServletException, IOException {
		InputStream stream = getResourceInputStream(resourcePath);
		if(stream != null) {
			byte[] data = IOUtils.toByteArray(stream);
			stream.close();
			return data;
		}else {
			return null;
		}
	}

	/**根据资源url路径，获取资源的InputStream<br>
	 * 资源固定存放在classpath下的public、static、resources、META-INF/resources目录下，或者WEB-INF目录下的文件（不能是WEB-INF目录）
	 * 功能描述:
	 * @param resourcePath
	 * @return
	 * @throws IOException 
	 * @author: wanghua
	 */
	private InputStream getResourceInputStream(String resourcePath) throws IOException {
		resourcePath = resourcePath.replace('\\', '/');
		int spIdx = resourcePath.indexOf("#");
		if(spIdx >=0) {
			resourcePath = resourcePath.substring(0, spIdx);
		}
		spIdx = resourcePath.indexOf("?");
		if(spIdx >=0) {
			resourcePath = resourcePath.substring(0, spIdx);
		}
		//特殊文件不允许读取
		String resourcePathLower = resourcePath.toLowerCase();
		for(String protectedFile : protectedFiles) {
			if(resourcePathLower.endsWith(protectedFile)) {
				return null;
			}
		}
		String filePath0 = resourcePath;
		if (!filePath0.startsWith("/")) {
			filePath0 = "/" + filePath0;
		}
		String resourcePathUpper = resourcePath.toUpperCase();
		boolean isProtectDir = resourcePathUpper.contains("/WEB-INF/") || resourcePathUpper.contains("/META-INF/");
		InputStream resourceStream = null;
		for (String fileDir : resourceDirs) {
			resourceStream=getInputStream(fileDir+filePath0,isProtectDir);
			if (resourceStream!=null) {
				return resourceStream;
			}
		}
		return getInputStream(filePath0,isProtectDir);
	}
	private InputStream getInputStream(String filePath,boolean isProtectDir) throws IOException {
		URL resource = ResourceServlet.class.getResource(filePath); //path不以’/'开头时，默认是从此类所在的包下取资源；path  以’/'开头时，则是从ClassPath根下获取；
		String realPath = null;
		if (resource == null && filePath.contains("..")) {
			if(!isProtectDir) {
				String basePath = ResourceServlet.class.getResource("/").getFile();
				String fullPath = basePath + filePath;
				File file = new File(fullPath);
				if (file.exists()) {
					realPath = fullPath;
				}
			}
		} else if (resource != null) {
			realPath = resource.getPath();
		}
		if (!StringUtils.isEmpty(realPath)) {				
			if (!realPath.contains("!")) {// 此文件在文件夹中，而不是在jar中，进行实时加载，即不进行缓存
				return resource.openStream();
			} else {
				return ResourceServlet.class.getResourceAsStream(filePath); // getResourceAsStream会对文件进行缓存
			}
		}
		return null;
	}

	private String welcomeFile;//"irest/view/main/index"首页地址;
	private String[] resourceDirs=new String[0];//资源目录配置
	@Override
	public void init(ServletConfig config) throws ServletException {
		this.welcomeFile = config.getInitParameter("welcomeFile");
		String dirs = config.getInitParameter("resourceDirs");
		String filePath1 = "../.."; // 优先级最低 根目录下的文件,../..对应 // WEB-INF/classes，即读取web根目录下的文件
		String filePath2 = "/public"; // 优先级最低
		String filePath3 = "/static";
		String filePath4 = "/resources";
		String filePath5 = "/META-INF/resources";// 优先级最高
		if(dirs==null||dirs.trim().isEmpty()) {
			resourceDirs = new String[] { filePath5, filePath4, filePath3, filePath2, filePath1};
		}else {
			List<String> dirList=new ArrayList<>();
			dirList.add(filePath5);
			dirList.add(filePath4);
			dirList.add(filePath3);
			dirList.add(filePath2);
			for(String dir:dirs.split(",")) {
				dir=dir.trim();
				if(dir.length()>1&&dir.startsWith("/")) {
					dirList.add(dir);
				}
			}
			dirList.add(filePath1);
			resourceDirs = dirList.toArray(resourceDirs);
		}
	}
	
}
